매크로 본문을 먼저 \ 없이 한 덩어리로 쓴다.
다 쓴 뒤 :%!clang-format 으로 \ 를 자동으로 붙이고 정렬한다.
clang-format 은 이미 \ 로 이어진 매크로의 \ 를 정렬해 준다.
\ 없이 줄바꿈만 한 매크로는 안 이어주므로, 이어 붙일 때만 \ 를 직접 친다.
1. 요구사항서
d0_warmup/src/ 에 헤더 4개 와 시험 대상 take.c / take.h 를 만든다.
정식판을 그대로 재현하는 게 아니라, 아래 표의 항목이 들어 있고 §3 테스트가 통과하면 합격이다.
guard.h 는 safe.h 를 include 한다.
그래서 #include "guard.h" 하나로 CHK_* 와 SAFE_* 를 모두 쓴다.
1-1. common.h
항목
요구사항
인클루드 가드
COMMON_H
ret_t
성공은 RET_OK = 0 하나, 실패는 전부 음수. 최소 5종: RET_FAIL(-1) / RET_INVALID_ARG(-2) / RET_NO_MEM(-3) / RET_IO_ERROR(-8) 과 RET_OK
ret_str(int)
코드를 이름 문자열로 돌려준다. "OK""FAIL""INVALID_ARG""NO_MEM""IO_ERROR" 모르는 값은 "UNKNOWN"
선언 방식
헤더에 두므로 static inline
1-2. log.h
항목
요구사항
레벨
log_level_t: LOG_LV_ERR < WRN < INF < DBG (값 0부터)
전역
extern log_level_t g_log_level; 실체는 .c 한 곳에만 정의한다.
LOG_ERR/WRN/INF/DBG
[시각][태그] 메시지 를 stderr 로 출력한다. 현재 레벨보다 낮은 로그는 출력하지 않는다. 가변 인자가 0개여도 컴파일된다.
LOG_ERRNO(fmt, ...)
errno 를 먼저 저장하고 메시지: strerror(errno)(errno) 를 LOG_ERR 로 출력한다.
1-3. safe.h
매크로
요구사항
SAFE_FREE(p)
NULL 이 아니면 free 후 p = NULL
SAFE_FCLOSE(fp)
NULL 이 아니면 fclose 후 fp = NULL
SAFE_CLOSE(fd)
fd >= 0 이면 close 후 fd = -1
SAFE_FCLOSE_RET(fp, rv)
SAFE_FCLOSE 와 같다. 추가로 fclose 가 실패하고 rv 가 아직 RET_OK 이면 rv = RET_IO_ERROR. 이미 실패한 rv 는 덮어쓰지 않는다.
(B) IS_ARRAY(a)
배열이면 1, 포인터면 0
(B) SAFE_CPY(dst, src)
항상 널 종단이 되는 snprintf 복사. src 가 NULL 이면 빈 문자열. dst 가 포인터면 컴파일 에러.
1-4. guard.h
모든 매크로는 do { } while (0) 이고, 인자는 ( ) 로 감싸며, 인자를 두 번 평가하지 않는다.
로그에는 인자 이름(#)과 __func__, __LINE__ 을 같이 찍는다.
매크로
조건
동작
CHK_PTR(p)
p == NULL
로그 + return RET_INVALID_ARG
CHK_ALLOC(p)
p == NULL
로그 + return RET_NO_MEM
CHK_RANGE(v, min, max)
범위 밖
로그(이름·값·범위) + return RET_INVALID_ARG
CHK_RET(expr)
expr != RET_OK
로그 + 그 값 그대로return
CHK_PTR_GOTO(p, rv, label)
p == NULL
rv = RET_INVALID_ARG, 로그, goto label
CHK_ALLOC_GOTO(p, rv, label)
p == NULL
rv = RET_NO_MEM, 로그, goto label
CHK_RET_GOTO(expr, rv, label)
rv = expr 가 RET_OK 가 아님
로그, goto label
CHK_COND_GOTO(cond, code, rv, label)
cond 가 거짓
rv = code, 로그, goto label
(B) CHK_STR(s)
NULL 또는 빈 문자열
로그 + return RET_INVALID_ARG
(B) CHK_STR_GOTO(s, rv, label)
NULL 또는 빈 문자열
rv = RET_INVALID_ARG, 로그, goto label
(B) CHK_RANGE_GOTO(v, min, max, rv, label)
범위 밖
rv = RET_INVALID_ARG, 로그, goto label
(C) CHK_NOT_NEG(fd)
fd < 0
로그 + return RET_INVALID_ARG (직접 설계, 이름은 CHK_NOT_NEG)
1-5. 시험 대상 take.c / take.h
함수
요구사항
int take_ptr(const char *s, int v)
s 가 NULL 이거나 v 가 0~100 밖이면 RET_INVALID_ARG. 통과하면 로그를 남기고 RET_OK.
int take_res(const char *path)
path NULL 은 RET_INVALID_ARG (즉시 return). 64바이트를 calloc 하고 path 를 읽기로 연다. 할당 실패는 RET_NO_MEM, 열기 실패는 RET_IO_ERROR (원인은 LOG_ERRNO). 하위 호출 take_ptr("inner", 10) 의 실패는 그대로 전파한다. 어느 경로로 나가든 누수가 없다 (goto CLEANUP 단일 출구).
1-6. 빌드
-Wall -Wextra -Werror -std=c11 에서 경고 0.
localtime_r 때문에 -D_POSIX_C_SOURCE=200809L 이 필요하다.
-fsanitize=address,undefined 리포트 0.
2. pseudo code 템플릿 — 보고 쳐서 익힌다
사용법
아래 뼈대를 d0_warmup/src/ 의 각 파일에 주석만 복사한다.
코드는 주석을 보고 내가 쓴다.
막히면 man / K 로 찾고, 그래도 막히면 정답지(1번 문서)나 기준 헤더를 연다.
[ ] 는 다 쓰면 x 로 바꾼다.
남은 항목은 grep -n '\[ \]' *.h 로 모아 볼 수 있다.
Level 1 — 주석 뼈대 (1회차, 전부 본다)
common.h
/* common.h — 프로젝트 공통 * * [필요한 것 → 헤더] (사용하는 것만 include, 사용처를 옆에 적는다) * - printf, FILE, fopen ............ <stdio.h> * - calloc, free .................. <stdlib.h> * - memset, strlen ................ <string.h> * - uint8_t ... 고정폭 정수 ........ <stdint.h> * - errno, strerror(errno) ........ <errno.h> *//* [ ] 1. 인클루드 가드: COMMON_H (파일 맨 위 / 맨 아래 주석 포함) *//* [ ] 2. 위 표의 헤더 include *//* [ ] 3. 리턴코드 enum (ret_t) * - 성공은 0 하나, 실패는 전부 음수 * - 최소 5종: 성공 / 일반실패 / 잘못된 인자 / 메모리 부족 / IO 에러 * - 마지막 줄 쉼표, 이름은 RET_ 접두어, typedef 이름은 ret_t *//* [ ] 4. ret_str(int) — 코드를 문자열로 * - 헤더에 두므로 'static inline' (왜? .c 가 여러 개여도 중복 정의 안 나게) * - switch 의 마지막은 default: "UNKNOWN" */
log.h
/* log.h — 최소 로그 * * [필요한 것 → 헤더] * - fprintf, stderr ............... <stdio.h> * - strerror ...................... <string.h> * - errno ......................... <errno.h> * - time, localtime_r, strftime ... <time.h> (localtime_r 이 왜 _r 인지 man 으로 확인) * -std=c11 이면 localtime_r 이 선언되지 않는다 → 빌드에 -D_POSIX_C_SOURCE=200809L *//* [ ] 1. 인클루드 가드: LOG_H *//* [ ] 2. 로그 레벨 enum (log_level_t): ERR < WRN < INF < DBG (값 0부터) *//* [ ] 3. 전역 레벨 변수 선언 * - 'extern' 만 쓴다 (실체는 .c 에 딱 하나) * - 이유를 한 줄로 적어본다 *//* [ ] 4. LOG_PRINT(레벨, 태그, 포맷, ...) 공통 매크로 * - 전체를 do { } while (0) 로 감싼다 * - 현재 레벨보다 낮은 로그는 출력하지 않는다 (if 를 매크로 안에서) * - 시각 문자열 "YYYY-MM-DD HH:MM:SS" (버퍼 20바이트) * - stderr 로 "[시각][태그] 메시지\n" * - 가변 인자가 0개여도 컴파일돼야 한다 → ##__VA_ARGS__ (왜? 앞 쉼표 제거) * - 매크로 인자 이름은 _ 로 시작 (_lv, _fmt ...) : 지역변수와 안 겹치게 *//* [ ] 5. LOG_ERR / LOG_WRN / LOG_INF / LOG_DBG 4개 * - LOG_PRINT 에 레벨과 태그 문자열만 바꿔 넘기는 한 줄짜리 *//* [ ] 6. LOG_ERRNO(_fmt, ...) : "메시지: strerror(errno)(errno)" 를 LOG_ERR 로 * - 첫 줄에서 int _e = errno; 로 먼저 저장한다 * (왜? LOG_PRINT 안의 time/localtime_r 이 errno 를 바꿀 수 있다) */
safe.h
/* safe.h — 해제 매크로 * * [필요한 것 → 헤더] * - free ........................... <stdlib.h> * - fclose ......................... <stdio.h> * - close .......................... <unistd.h> (man 2 close 로 확인) * - RET_IO_ERROR ................... common.h * * [공통 규칙] * - do { } while (0), 정의 끝에 ';' 없음 * - 인자는 단순 변수만 (SAFE_FREE(p++) 금지) *//* [ ] 1. 인클루드 가드: SAFE_H, include 는 위 표대로 *//* [ ] 2. SAFE_FREE(_p) : NULL 아니면 free 후 NULL 대입 *//* [ ] 3. SAFE_FCLOSE(_fp) : NULL 아니면 fclose 후 NULL 대입 *//* [ ] 4. SAFE_CLOSE(_fd) : 0 이상이면 close 후 -1 대입 (fd 는 '-1' 이 무효값) *//* [ ] 5. SAFE_FCLOSE_RET(_fp,_rv) : 쓰기 파일용 * - fclose 가 0 이 아니고 _rv 가 아직 RET_OK 일 때만 _rv = RET_IO_ERROR * (왜 '아직 RET_OK 일 때만'? 먼저 난 실패의 원인을 보존하려고) * - 어느 쪽이든 마지막에 _fp = NULL *//* [B 회차] IS_ARRAY(_a) : __builtin_types_compatible_p + __typeof__ (GCC/Clang 전용) *//* [B 회차] SAFE_CPY(_dst,_src) : 항상 널 종단 snprintf. * _dst 가 포인터면 컴파일 에러가 나야 한다 * (sizeof(char[IS_ARRAY(_dst) ? 1 : -1]) 트릭) * — 왜 포인터면 안 되나? */
guard.h
/* guard.h — 가드 매크로 * * [필요한 것] * - common.h : RET_ 코드, ret_str * - log.h : LOG_ERR * - safe.h : SAFE_* (guard.h 가 같이 include) * * [공통 규칙] 모든 매크로에 적용 * - do { } while (0) 로 감싼다. 정의 끝에 ';' 를 붙이지 않는다 * - 인자는 쓸 때마다 ( ) 로 감싼다 * - 인자를 두 번 평가하지 않는다 (함수 호출이 들어와도 한 번만 실행) * - 로그에 "인자 이름" 과 __func__, __LINE__ 을 같이 찍는다 (# 연산자) *//* [ ] 1. 인클루드 가드: GUARD_H *//* [ ] 2. #include 3종 (common.h / log.h / safe.h) *//* ── 즉시 return 판 (자원을 잡기 전) ── *//* [ ] 3. CHK_PTR(_p) : NULL 이면 로그 + RET_INVALID_ARG 리턴 *//* [ ] 4. CHK_RANGE(_v,_min,_max) : 범위 밖이면 로그(이름·값·범위) + RET_INVALID_ARG * 값은 (int) 캐스팅 후 %d 로 *//* [ ] 5. CHK_RET(_expr) : _expr 의 결과가 RET_OK 가 아니면 로그 + 그 값 그대로 리턴 * (지역변수 하나에 담아 한 번만 평가) *//* ── goto 판 (자원을 잡은 후) — 함수 안에 'int ret' 가 있다고 가정 ── *//* [ ] 6. CHK_PTR_GOTO(_p,_rv,_label) : NULL → _rv 세팅, 로그, goto *//* [ ] 7. CHK_RET_GOTO(_expr,_rv,_label) : _rv = _expr; 실패면 로그, goto *//* [ ] 8. CHK_COND_GOTO(_cond,_code,_rv,_label) : 조건이 거짓이면 _rv = _code, 로그, goto *//* ── 할당 판 (할당 실패는 INVALID_ARG 가 아니라 NO_MEM) ── *//* [ ] 9. CHK_ALLOC(_p) : NULL 이면 로그 + RET_NO_MEM 리턴 *//* [ ] 10. CHK_ALLOC_GOTO(_p,_rv,_label) : NULL → _rv = RET_NO_MEM, 로그, goto *//* ※ 해제 판(SAFE_*)은 safe.h 로 분리했다. guard.h 는 safe.h 를 include 한다. *//* [B 회차] CHK_STR(_s) / CHK_STR_GOTO(_s,_rv,_label) : NULL 또는 빈 문자열이면 INVALID_ARG *//* [B 회차] CHK_RANGE_GOTO(_v,_min,_max,_rv,_label) : 범위 밖이면 로그 + _rv = INVALID_ARG, goto *//* [C 회차] CHK_NOT_NEG(_fd) : 직접 설계 (음수면 로그 + RET_INVALID_ARG) */
take.c
/* take.c — 위 매크로를 전부 한 번씩 호출해서 확인 * * [필요한 것] * - common.h / log.h / guard.h (순서: 내 헤더는 표준 헤더 뒤) * - g_log_level 의 실체는 여기가 아니라 테스트(test_take.c) 에 있다 ← extern 선언의 짝 *//* [ ] 1. take_ptr(const char *s, int v) — 즉시 return 판 * - CHK_PTR(s) * - CHK_RANGE(v, 0, 100) * - 통과하면 LOG_INF 후 RET_OK *//* [ ] 2. take_res(const char *path) — goto 판 * ① ret 은 실패로 시작 (int ret = RET_FAIL) * ② 포인터는 NULL, FILE* 도 NULL 로 선언 * ③ 자원 잡기 전 검사는 CHK_PTR (즉시 return) * ④ calloc 뒤에는 CHK_ALLOC_GOTO (NO_MEM) * ⑤ fopen 이 NULL 이면 LOG_ERRNO, 이어서 CHK_COND_GOTO (IO_ERROR) * ⑥ 하위 호출은 CHK_RET_GOTO (take_ptr("inner", 10)) * ⑦ 끝까지 오면 ret = RET_OK * ⑧ CLEANUP: 잡은 '역순' 으로 SAFE_ 호출, return ret * (쓰기 파일이면 SAFE_FCLOSE_RET) */
아무것도 안 본다.§1 요구사항서와 §3 테스트만 보고 친다.(C) CHK_NOT_NEG(_fd) 는 §1-4 의 한 줄 요구사항으로 직접 설계한다.
막혔을 때 찾는 순서
컴파일 에러 문구를 본다.
unknown type name / implicit declaration 이면 include 누락이다.
man 3 <함수> 를 연다 (vim 안에서는 커서를 단어에 놓고 K).
SYNOPSIS 맨 위의 #include 를 확인한다.
매크로가 이상하면 gcc -E d0_warmup/src/take.c | less 로 전처리 결과를 눈으로 본다.
그래도 안 되면 정답지(1번 문서)의 해당 매크로만 본다.
전체를 보지 않는다.
사용처 찾기
함수/매크로 위에 커서를 두고 :grep -rnw <Ctrl+R><Ctrl+W> . 를 실행한 뒤 :copen 을 연다.
정의는 ctags -R . 후 Ctrl+] / Ctrl+T 로 찾는다.
#define 매크로도 Ctrl+] 로 정의를 찾는다 (universal-ctags 는 기본 포함).
헤더의 prototype 까지 태그에 넣으려면 ctags -R --c-kinds=+p . 를 쓴다.
3. 테스트 코드 — 이게 통과하면 끝
d0_warmup/src/common.h log.h safe.h guard.h 헤더 4개d0_warmup/src/take.h take_ptr / take_res 선언d0_warmup/src/take.c §2 take.c 구현d0_warmup/test/test_take.c 아래 시험 (g_log_level 정의 포함)
시험의 형태 (D1~D9 공통)
정상 한 가지, 경계 여러 가지, NULL / 실패 경로를 각각 확인한다.
D0 에서는 이 셋을 매크로 하나하나에 적용한다.
매크로는 return / goto 를 호출한 함수 안에서 실행하므로, 시험용 작은 함수(t_*)를 만들어 그 반환값을 확인한다.
3-0. 공통 매크로와 시험 도우미
#include <stdio.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include "common.h"#include "log.h"#include "guard.h"#include "take.h"log_level_t g_log_level = LOG_LV_INF; /* ★ extern 선언의 실체는 .c 에 딱 하나 */static int g_fail = 0;#define T_EQ_INT(_got, _exp) \ do { \ long long _g = (long long)(_got), _e = (long long)(_exp); \ if (_g != _e) { g_fail++; \ printf(" X %s:%d got=%lld exp=%lld\n", \ __func__, __LINE__, _g, _e); } \ } while (0)#define T_EQ_STR(_got, _exp) \ do { \ if (strcmp((_got), (_exp)) != 0) { g_fail++; \ printf(" X %s:%d got=\"%s\" exp=\"%s\"\n", \ __func__, __LINE__, (_got), (_exp)); } \ } while (0)static int g_cnt = 0;static int t_count_fail(void) { g_cnt++; return RET_FAIL; } /* 호출 횟수 세기용 */
3-A. 변형 A 시험
static int t_alloc(void *p) { CHK_ALLOC(p); return RET_OK; }static int t_if_else(const char *p, int f){ if (f) CHK_PTR(p); /* do-while(0) 가 아니면 이 else 가 깨진다 */ else f = 2; return RET_OK;}static int t_chk_ret(void) { CHK_RET(t_count_fail()); return RET_OK; }static int t_ptr_goto(void *p, int *cleaned){ int ret = RET_FAIL; *cleaned = 0; CHK_PTR_GOTO(p, ret, CLEANUP); ret = RET_OK;CLEANUP: *cleaned = 1; return ret;}static int t_alloc_goto(void *p){ int ret = RET_FAIL; CHK_ALLOC_GOTO(p, ret, CLEANUP); ret = RET_OK;CLEANUP: return ret;}static int t_ret_goto(void){ int ret = RET_FAIL; CHK_RET_GOTO(t_count_fail(), ret, CLEANUP); ret = RET_OK;CLEANUP: return ret;}static int t_cond_goto(int ok){ int ret = RET_FAIL; CHK_COND_GOTO(ok, RET_IO_ERROR, ret, CLEANUP); ret = RET_OK;CLEANUP: return ret;}static void test_common_log(void){ T_EQ_INT(RET_OK, 0); /* 성공은 0 */ T_EQ_INT(RET_FAIL < 0, 1); /* 실패는 전부 음수 */ T_EQ_INT(RET_INVALID_ARG < 0, 1); T_EQ_INT(RET_NO_MEM < 0, 1); T_EQ_INT(RET_IO_ERROR < 0, 1); T_EQ_STR(ret_str(RET_OK), "OK"); T_EQ_STR(ret_str(RET_INVALID_ARG), "INVALID_ARG"); T_EQ_STR(ret_str(RET_NO_MEM), "NO_MEM"); T_EQ_STR(ret_str(RET_IO_ERROR), "IO_ERROR"); T_EQ_STR(ret_str(-999), "UNKNOWN");}static void test_guard(void){ int x = 0, cleaned = 0; T_EQ_INT(t_alloc(NULL), RET_NO_MEM); /* 할당 실패는 NO_MEM */ T_EQ_INT(t_alloc(&x), RET_OK); T_EQ_INT(t_if_else(NULL, 1), RET_INVALID_ARG); /* if/else 안에서도 안전 */ T_EQ_INT(t_if_else(NULL, 0), RET_OK); g_cnt = 0; T_EQ_INT(t_chk_ret(), RET_FAIL); /* 실패 값을 그대로 전달 */ T_EQ_INT(g_cnt, 1); /* ★ 인자는 한 번만 평가 */ T_EQ_INT(t_ptr_goto(NULL, &cleaned), RET_INVALID_ARG); T_EQ_INT(cleaned, 1); /* goto 로 CLEANUP 에 도착 */ T_EQ_INT(t_ptr_goto(&x, &cleaned), RET_OK); T_EQ_INT(t_alloc_goto(NULL), RET_NO_MEM); T_EQ_INT(t_alloc_goto(&x), RET_OK); g_cnt = 0; T_EQ_INT(t_ret_goto(), RET_FAIL); T_EQ_INT(g_cnt, 1); /* ★ 한 번만 평가 */ T_EQ_INT(t_cond_goto(0), RET_IO_ERROR); /* 넘긴 코드가 rv 에 들어간다 */ T_EQ_INT(t_cond_goto(1), RET_OK);}static void test_safe(void){ char *p = calloc(1, 8); FILE *fp = tmpfile(); int fd = open("/dev/null", O_RDONLY); int ret = RET_OK; FILE *fp2 = tmpfile(); SAFE_FREE(p); T_EQ_INT(p == NULL, 1); /* 해제 후 NULL */ SAFE_FREE(p); /* 두 번 호출해도 안전 */ SAFE_FCLOSE(fp); T_EQ_INT(fp == NULL, 1); SAFE_FCLOSE(fp); SAFE_CLOSE(fd); T_EQ_INT(fd, -1); /* 무효값은 -1 */ SAFE_CLOSE(fd); T_EQ_INT(fd, -1); SAFE_FCLOSE_RET(fp2, ret); T_EQ_INT(fp2 == NULL, 1); T_EQ_INT(ret, RET_OK); /* 성공이면 ret 그대로 */ fp2 = tmpfile(); ret = RET_NO_MEM; SAFE_FCLOSE_RET(fp2, ret); T_EQ_INT(ret, RET_NO_MEM); /* ★ 먼저 난 실패는 보존 */}static void test_take(void){ T_EQ_INT(take_ptr("hi", 10), RET_OK); /* 정상 */ T_EQ_INT(take_ptr("hi", 0), RET_OK); /* 경계: 하한 */ T_EQ_INT(take_ptr("hi", 100), RET_OK); /* 경계: 상한 */ T_EQ_INT(take_ptr("hi", -1), RET_INVALID_ARG); /* 한 칸 아래 */ T_EQ_INT(take_ptr("hi", 101), RET_INVALID_ARG); /* 한 칸 위 */ T_EQ_INT(take_ptr(NULL, 10), RET_INVALID_ARG); /* 로그에 "s is NULL" */ T_EQ_INT(take_res("/dev/null"), RET_OK); /* 정상 */ T_EQ_INT(take_res("/no/such/file"), RET_IO_ERROR);/* 로그에 LOG_ERRNO, 누수 없이 */ T_EQ_INT(take_res(NULL), RET_INVALID_ARG); /* 자원 잡기 전 즉시 return */}int main(void){ test_common_log(); test_guard(); test_safe(); test_take(); printf(g_fail ? "FAIL %d\n" : "PASS\n", g_fail); return g_fail ? 1 : 0;}
3-B. 변형 B 추가 시험
3-A 의 main() 에 test_b() 호출을 한 줄 더한다.
static int t_str(const char *s) { CHK_STR(s); return RET_OK; }static int t_str_goto(const char *s){ int ret = RET_FAIL; CHK_STR_GOTO(s, ret, CLEANUP); ret = RET_OK;CLEANUP: return ret;}static int t_range_goto(int v){ int ret = RET_FAIL; CHK_RANGE_GOTO(v, 1, 10, ret, CLEANUP); ret = RET_OK;CLEANUP: return ret;}static void test_b(void){ char buf[4]; T_EQ_INT(t_str("a"), RET_OK); T_EQ_INT(t_str(""), RET_INVALID_ARG); /* 빈 문자열 */ T_EQ_INT(t_str(NULL), RET_INVALID_ARG); T_EQ_INT(t_str_goto("a"), RET_OK); T_EQ_INT(t_str_goto(""), RET_INVALID_ARG); T_EQ_INT(t_str_goto(NULL), RET_INVALID_ARG); T_EQ_INT(t_range_goto(1), RET_OK); /* 경계 */ T_EQ_INT(t_range_goto(10), RET_OK); T_EQ_INT(t_range_goto(0), RET_INVALID_ARG); T_EQ_INT(t_range_goto(11), RET_INVALID_ARG); SAFE_CPY(buf, "abcdef"); T_EQ_STR(buf, "abc"); /* 항상 널 종단 (잘림) */ SAFE_CPY(buf, "ab"); T_EQ_STR(buf, "ab"); SAFE_CPY(buf, NULL); T_EQ_STR(buf, ""); /* NULL 은 빈 문자열 */ T_EQ_INT(IS_ARRAY(buf), 1);}
컴파일 에러를 눈으로 확인한다
char *q = buf; SAFE_CPY(q, "x"); 를 한 줄 넣어 보면 컴파일 에러가 나야 한다.
확인했으면 그 줄은 지운다 (남기면 빌드가 깨진다).
3-C. 변형 C 추가 시험
CHK_NOT_NEG 는 요구사항서의 한 줄만 보고 직접 만든 것이므로, 아래 시험으로 동작을 확인한다.
static int t_not_neg(int fd) { CHK_NOT_NEG(fd); return RET_OK; }static void test_c(void){ T_EQ_INT(t_not_neg(0), RET_OK); /* 경계: 0 은 정상 */ T_EQ_INT(t_not_neg(5), RET_OK); T_EQ_INT(t_not_neg(-1), RET_INVALID_ARG); /* 한 칸 아래 */ T_EQ_INT(t_not_neg(-100), RET_INVALID_ARG);}